package nl.science.uva.pico.compiler;

import static org.apache.commons.lang.StringUtils.isEmpty;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;

/** 
 * Main entry point for the Compiler  
 * 
 * @author Ivo van Dongen, Karel Pieterson
 * @version 1.0
 */
public class Main {

	//Named constants
	private static final int EXIT_VALUE_SUCCESS = 0;
	private static final int EXIT_VALUE_ERROR = 1;
	private static final String OPTION_HELP_LONG = "help";
	private static final String OPTION_HELP_SHORT = "h";
	private static final String OPTION_INPUT_FILE_LONG = "input-file";
	private static final String OPTION_INPUT_FILE_SHORT = "i";
	private static final String OPTION_LOGGING_SHORT = "l";
	private static final String OPTION_LOGGING_LONG = "enable-logging";
	private static final String OPTION_OUTPUT_FILE_LONG = "out-file";
	private static final String OPTION_OUTPUT_FILE_SHORT = "o";
	private static final String PROGRAM_NAME = "PicoCompiler";

	/**
	 * @param arguments the command line arguments
	 */
	public static void main(String[] arguments) {
		//Create our options definition
		Options options  = getCommandlineOptions();
		
		//Parse the command line options
		CommandLine commandLine = parseCommandLineInput(options, arguments);
		
		//If help is requested provide it otherwise run the interpreter
		if (commandLine.hasOption(OPTION_HELP_SHORT)) {
			printHelpMessage(options);
			System.exit(EXIT_VALUE_SUCCESS);
		}
		
		//Initialize the logging system
		BasicConfigurator.configure();
		//Disable the logger if not requested explicitely
		if (!commandLine.hasOption(OPTION_LOGGING_SHORT)) {
			Logger.getRootLogger().setLevel(Level.OFF);
		}
		
		//Create the requested input stream
		InputStream inputStream = createInputStream(commandLine);
		
		//Create the requested output stream
		OutputStream outputStream = createOutputStream(commandLine);
		
		//Compile the stream
		Compiler compiler = new Compiler();
		String compiled = null;
		try {
			compiled = compiler.compileStream(inputStream);
		} catch (IOException e) {
			System.err.println("Compile error: " + e.getMessage());
			System.exit(EXIT_VALUE_ERROR);
		}
		
		//Write the result to the outputstream
		try {
			IOUtils.copy(new ByteArrayInputStream(compiled.getBytes()), outputStream);
		} catch (IOException e) {
			System.err.println("Error writing result to requested output: " + e.getMessage());
			System.exit(EXIT_VALUE_ERROR);
		}
		
		//Success!
		System.exit(EXIT_VALUE_SUCCESS);
	}
	
	
	/**
	 * Parses the command line input and handles errors if any
	 * 
	 * @param options the declared {@link Options}
	 * @param args the command line arguments
	 * @return the {@link CommandLine} representation
	 */
	private static CommandLine parseCommandLineInput(Options options, String[] arguments) {
		CommandLineParser commandLineParser = new PosixParser();
		CommandLine commandLine = null;
		try {
			commandLine = commandLineParser.parse(options, arguments);
		} catch (ParseException e) {
			//On error display it and show the help message
			System.err.println("Invalid parameters: " + e.getMessage());
			printHelpMessage(options);
			System.exit(EXIT_VALUE_ERROR);
		}
		return commandLine;
	}

	/**
	 * Creates the {@link InputStream} as requested on the command line and handles errors
	 * 
	 * @param commandLine the {@link CommandLine}
	 * @return the {@link InputStream}
	 */
	private static InputStream createInputStream(CommandLine commandLine) {
		if (commandLine.hasOption(OPTION_INPUT_FILE_SHORT)) {
			//Requested to use a file as input
			try {
				return createFileInputStream(commandLine.getOptionValue(OPTION_INPUT_FILE_SHORT));
			} catch (IOException e) {
				System.err.println("Could not read input file, cause: " + e.getMessage());
				System.exit(EXIT_VALUE_ERROR);
			}
		}
		
		//Default to standard in
		return System.in;
	}
	
	/**
	 * Creates the {@link OutputStream} as requested on the command line and handles errors
	 * 
	 * @param commandLine the {@link CommandLine}
	 * @return the {@link OutputStream}
	 */
	private static OutputStream createOutputStream(CommandLine commandLine) {
		if (commandLine.hasOption(OPTION_OUTPUT_FILE_SHORT)) {
			//Requested to use a file as output
			try {
				return createFileOutputStream(commandLine.getOptionValue(OPTION_OUTPUT_FILE_SHORT));
			} catch (IOException e) {
				System.err.println("Cannot write to output file, cause: " + e.getMessage());
				System.exit(EXIT_VALUE_ERROR);
			}
		}
		
		//Default to standard out
		return System.out;
	}
	
	/**
	 * Creates a {@link FileOutputStream} for the given file name and checks writeability
	 * 
	 * @param fileName the file to write to
	 * @return the {@link FileOutputStream}
	 * 
	 * @throws IOException
	 */
	private static OutputStream createFileOutputStream(String fileName) throws IOException {
		if (isEmpty(fileName)) {
			throw new IOException("Filename is empty");
		}
		
		File outputFile = new File(fileName);
		
		//create the file if it doesn't exist
		if (!outputFile.exists()) {
			outputFile.createNewFile();
		}
		
		//Return the output stream if we can write to it
		if (outputFile.canWrite()) {
			return new FileOutputStream(outputFile);
		} else {
			throw new IOException("Cannot write to file: " + fileName);
		}
	}

	/**
	 * Creates a {@link FileInputStream} for the given file name and checks readability
	 * 
	 * @param fileName the file to read from
	 * @return the {@link FileInputStream}
	 * 
	 * @throws IOException
	 */
	private static InputStream createFileInputStream(String fileName) throws IOException {
		if (isEmpty(fileName)) {
			throw new IOException("Filename is empty");
		}
		
		File inputFile = new File(fileName);
		
		if (inputFile.exists() && inputFile.canRead()) {
			return new FileInputStream(inputFile);
		} else {
			throw new IOException("Cannot read file: " + fileName);
		}
		
	}

	/**
	 * Prints a help message to system out
	 * @param options the {@link Options} to print
	 */
	private static void printHelpMessage(Options options) {
		HelpFormatter formatter = new HelpFormatter();
		formatter.printHelp(PROGRAM_NAME, options);
	}

	/**
	 * Creates the {@link Options} declared for this program
	 * @return the {@link Options}
	 */
	private static Options getCommandlineOptions() {
		Options options = new Options();
		options.addOption(OPTION_HELP_SHORT,OPTION_HELP_LONG, false, "Display this help message");
		options.addOption(OPTION_INPUT_FILE_SHORT, OPTION_INPUT_FILE_LONG, true, "Use the given file as input (instead of standard in)");
		options.addOption(OPTION_OUTPUT_FILE_SHORT, OPTION_OUTPUT_FILE_LONG, true, "Use the given file as output (instead of standard out)");
		options.addOption(OPTION_LOGGING_SHORT, OPTION_LOGGING_LONG, false, "Enable debug logging");
		return options;
	}
}
